/******************************************************************************* * 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 & implementation * *******************************************************************************/ package org.eclipse.gef.common.tests; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import org.eclipse.gef.common.beans.property.ReadOnlyMapWrapperEx; import org.eclipse.gef.common.beans.property.SimpleMapPropertyEx; import org.eclipse.gef.common.tests.ObservableSetMultimapTests.InvalidationExpector; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import com.google.inject.Provider; import javafx.beans.property.MapProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.MapChangeListener; import javafx.collections.ObservableMap; @RunWith(Parameterized.class) public class MapPropertyExTests { protected static class ChangeExpector<K, V> implements ChangeListener<ObservableMap<K, V>> { private ObservableValue<ObservableMap<K, V>> source; private LinkedList<ObservableMap<K, V>> oldValueQueue = new LinkedList<>(); private LinkedList<ObservableMap<K, V>> newValueQueue = new LinkedList<>(); public ChangeExpector(ObservableValue<ObservableMap<K, V>> source) { this.source = source; } public void addExpectation(ObservableMap<K, V> oldValue, ObservableMap<K, V> newValue) { // We check that the reference to the observable value is correct, // thus do not copy the passed in values. oldValueQueue.addFirst(oldValue); newValueQueue.addFirst(newValue); } @Override public void changed( ObservableValue<? extends ObservableMap<K, V>> observable, ObservableMap<K, V> oldValue, ObservableMap<K, V> newValue) { if (oldValueQueue.size() <= 0) { fail("Received unexpected change."); } assertEquals(source, observable); assertEquals(oldValueQueue.pollLast(), oldValue); assertEquals(newValueQueue.pollLast(), newValue); } public void check() { if (oldValueQueue.size() > 0) { fail("Did not receive " + oldValueQueue.size() + " expected changes."); } } } protected static class MapChangeExpector<K, V> implements MapChangeListener<K, V> { private ObservableMap<K, V> source; private LinkedList<K> keyQueue = new LinkedList<>(); private LinkedList<V> addedValueQueue = new LinkedList<>(); private LinkedList<V> removedValueQueue = new LinkedList<>(); public MapChangeExpector(ObservableMap<K, V> source) { this.source = source; } public void addExpectation(K key, V removedValue, V addedValue) { keyQueue.addFirst(key); addedValueQueue.addFirst(addedValue); removedValueQueue.addFirst(removedValue); } public void check() { if (keyQueue.size() > 0) { fail("Did not receive " + keyQueue.size() + " expected changes."); } } @Override public void onChanged( MapChangeListener.Change<? extends K, ? extends V> change) { if (keyQueue.size() <= 0) { fail("Received unexpected change " + change); } assertEquals(source, change.getMap()); // check key K expectedKey = keyQueue.pollLast(); assertEquals(expectedKey, change.getKey()); // check added values V expectedAddedValue = addedValueQueue.pollLast(); assertEquals(expectedAddedValue, change.getValueAdded()); if (expectedAddedValue != null) { assertTrue(change.wasAdded()); } else { assertFalse(change.wasAdded()); } // check removed values V expectedRemovedValue = removedValueQueue.pollLast(); assertEquals(expectedRemovedValue, change.getValueRemoved()); if (expectedRemovedValue != null) { assertTrue(change.wasRemoved()); } else { assertFalse(change.wasRemoved()); } // check string representation if (expectedAddedValue == null && expectedRemovedValue != null) { assertEquals("Removed " + expectedRemovedValue + " for key " + expectedKey + ".", change.toString()); } else if (expectedAddedValue != null && expectedRemovedValue == null) { assertEquals("Added " + expectedAddedValue + " for key " + expectedKey + ".", change.toString()); } else { assertEquals("Replaced " + expectedRemovedValue + " by " + expectedAddedValue + " for key " + expectedKey + ".", change.toString()); } } } @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { new Provider<MapProperty<Integer, String>>() { @Override public MapProperty<Integer, String> get() { // Replacement for SimpleMapProperty which fixes // https://bugs.openjdk.java.net/browse/JDK-8136465) return new SimpleMapPropertyEx<>(FXCollections .observableMap(new HashMap<Integer, String>())); } } }, { new Provider<MapProperty<Integer, String>>() { @Override public MapProperty<Integer, String> get() { // Replacement for ReadOnlyMapWrapper which fixes // https://bugs.openjdk.java.net/browse/JDK-8136465) return new ReadOnlyMapWrapperEx<>(FXCollections .observableMap(new HashMap<Integer, String>())); } } } }); } private Provider<MapProperty<Integer, String>> propertyProvider; public MapPropertyExTests( Provider<MapProperty<Integer, String>> propertyProvider) { this.propertyProvider = propertyProvider; } @Test public void bidirectionalBinding() { MapProperty<Integer, String> property1 = propertyProvider.get(); MapProperty<Integer, String> property2 = propertyProvider.get(); // XXX: According to JavaFX contract, a bidirectional binding does not // lead to the properties being regarded as bound. property2.bindBidirectional(property1); assertFalse(property1.isBound()); assertFalse(property2.isBound()); // change value of first property ObservableMap<Integer, String> newValue = FXCollections .observableMap(new HashMap<Integer, String>()); newValue.put(1, "1-1"); property1.set(newValue); assertEquals(newValue, property1.get()); assertEquals(newValue, property2.get()); assertEquals(property1, property2); // change value of second property newValue = FXCollections.observableMap(new HashMap<Integer, String>()); newValue.put(2, "2-1"); property2.set(newValue); assertEquals(newValue, property1.get()); assertEquals(newValue, property2.get()); assertEquals(property1, property2); // unbind (ensure values are no longer synchronized) property2.unbindBidirectional(property1); assertFalse(property1.isBound()); assertFalse(property2.isBound()); newValue = FXCollections.observableMap(new HashMap<Integer, String>()); newValue.put(3, "3-1"); property1.set(newValue); assertEquals(newValue, property1.get()); assertNotEquals(newValue, property2.get()); assertNotEquals(property1, property2); // bind on null (yields NPE) try { property2.bindBidirectional(null); fail("Expected NullPointerException because binding to null is not valid."); } catch (NullPointerException e) { // expected } // unbind from null (yields NPE) try { property2.unbindBidirectional(null); fail("Expected NullPointerException because binding to null is not valid."); } catch (NullPointerException e) { // expected } // bind on itself (yields IAE) try { property2.bindBidirectional(property2); fail("Expected IllegalArgumentException because binding to itself is not valid."); } catch (IllegalArgumentException e) { // expected } // unbind from itself (yields IAE) try { property2.unbindBidirectional(property2); fail("Expected IllegalArgumentException because binding to itself is not valid."); } catch (IllegalArgumentException e) { // expected } } // TODO:change notifications @Test public void bidirectionalContentBinding() { MapProperty<Integer, String> property1 = propertyProvider.get(); MapProperty<Integer, String> property2 = propertyProvider.get(); // XXX: According to JavaFX contract, a bidirectional binding does not // lead to the properties being regarded as bound. property2.bindContentBidirectional(property1); assertFalse(property1.isBound()); assertFalse(property2.isBound()); // change value of first property ObservableMap<Integer, String> newValue = FXCollections .observableMap(new HashMap<Integer, String>()); newValue.put(1, "1-1"); property1.set(newValue); assertEquals(newValue, property1.get()); assertEquals(newValue, property2.get()); assertEquals(property1, property2); // change value of second property newValue = FXCollections.observableMap(new HashMap<Integer, String>()); newValue.put(2, "2-1"); property2.set(newValue); assertEquals(newValue, property1.get()); assertEquals(newValue, property2.get()); assertEquals(property1, property2); // unbind (ensure values are no longer synchronized) property2.unbindContentBidirectional(property1); assertFalse(property1.isBound()); assertFalse(property2.isBound()); newValue = FXCollections.observableMap(new HashMap<Integer, String>()); newValue.put(3, "3-1"); property1.set(newValue); assertEquals(newValue, property1.get()); assertNotEquals(newValue, property2.get()); assertNotEquals(property1, property2); // bind on null (yields NPE) try { property2.bindBidirectional(null); fail("Expected NullPointerException because binding to null is not valid."); } catch (NullPointerException e) { // expected } // unbind from null (yields NPE) try { property2.unbindBidirectional(null); fail("Expected NullPointerException because binding to null is not valid."); } catch (NullPointerException e) { // expected } // bind on itself (yields IAE) try { property2.bindBidirectional(property2); fail("Expected IllegalArgumentException because binding to itself is not valid."); } catch (IllegalArgumentException e) { // expected } // unbind from itself (yields IAE) try { property2.unbindBidirectional(property2); fail("Expected IllegalArgumentException because binding to itself is not valid."); } catch (IllegalArgumentException e) { // expected } } @Test public void changeListenerRegistrationAndDeregistration() { MapProperty<Integer, String> property = propertyProvider.get(); // register listener ChangeExpector<Integer, String> changeListener = null; changeListener = new ChangeExpector<>(property); property.addListener(changeListener); // add second listener (and remove again) ChangeExpector<Integer, String> changeListener2 = null; changeListener2 = new ChangeExpector<>(property); property.addListener(changeListener2); property.removeListener(changeListener2); ObservableMap<Integer, String> newValue = FXCollections .observableMap(new HashMap<Integer, String>()); changeListener.addExpectation(property.get(), newValue); newValue.put(1, "1"); property.set(newValue); changeListener.check(); } /** * Check change notifications for observed value changes are properly fired. */ @Test public void changeNotifications() { MapProperty<Integer, String> property = propertyProvider.get(); // initialize property ObservableMap<Integer, String> newValue = FXCollections .observableHashMap(); newValue.put(1, "1"); newValue.put(2, "2"); newValue.put(3, "3"); property.set(newValue); // register listener InvalidationExpector invalidationListener = new InvalidationExpector(); MapChangeExpector<Integer, String> mapChangeListener = new MapChangeExpector<>( property); ChangeExpector<Integer, String> changeListener = new ChangeExpector<>( property); property.addListener(invalidationListener); property.addListener(mapChangeListener); property.addListener(changeListener); // change property value (disjoint values) newValue = FXCollections.observableHashMap(); newValue.put(4, "4"); newValue.put(5, "5"); newValue.put(6, "6"); invalidationListener.expect(1); changeListener.addExpectation(property.get(), newValue); mapChangeListener.addExpectation(1, "1", null); mapChangeListener.addExpectation(2, "2", null); mapChangeListener.addExpectation(3, "3", null); mapChangeListener.addExpectation(4, null, "4"); mapChangeListener.addExpectation(5, null, "5"); mapChangeListener.addExpectation(6, null, "6"); property.set(newValue); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); // change property value (overlapping values) newValue = FXCollections.observableHashMap(); newValue.put(5, "55"); newValue.put(6, "6"); newValue.put(7, "7"); invalidationListener.expect(1); changeListener.addExpectation(property.get(), newValue); mapChangeListener.addExpectation(4, "4", null); mapChangeListener.addExpectation(5, "5", "55"); mapChangeListener.addExpectation(7, null, "7"); property.set(newValue); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); // change property value (change to null) invalidationListener.expect(1); changeListener.addExpectation(property.get(), null); mapChangeListener.addExpectation(5, "55", null); mapChangeListener.addExpectation(6, "6", null); mapChangeListener.addExpectation(7, "7", null); property.set(null); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); // set to null again (no expectation) property.set(null); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); // change property value (change from null) newValue = FXCollections.observableHashMap(); newValue.put(1, "1"); newValue.put(2, "2"); newValue.put(3, "3"); invalidationListener.expect(1); changeListener.addExpectation(null, newValue); mapChangeListener.addExpectation(1, null, "1"); mapChangeListener.addExpectation(2, null, "2"); mapChangeListener.addExpectation(3, null, "3"); property.set(null); property.set(newValue); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); // set to identical value (no notifications expected) property.set(newValue); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); // set to equal value (no list change notification expected) newValue = FXCollections.observableHashMap(); newValue.put(1, "1"); newValue.put(2, "2"); newValue.put(3, "3"); invalidationListener.expect(1); changeListener.addExpectation(property.get(), newValue); property.set(newValue); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); // change observed value (only invalidation and list change expected) // FIXME: ObservableMapWrapper fires an invalidation event for each // change to the map (whereas a change of the observable value leads to // a single notification). We could provide an own ObservableMap // implementation to fix this. invalidationListener.expect(3); mapChangeListener.addExpectation(1, "1", null); mapChangeListener.addExpectation(2, "2", null); mapChangeListener.addExpectation(3, "3", null); property.get().clear(); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); // touch observed value (don't change it) property.get().clear(); invalidationListener.check(); mapChangeListener.check(); changeListener.check(); } }