/******************************************************************************* * 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.Collections; import java.util.LinkedList; import org.eclipse.gef.common.beans.property.ReadOnlySetMultimapProperty; import org.eclipse.gef.common.beans.property.ReadOnlySetMultimapWrapper; import org.eclipse.gef.common.beans.property.SetMultimapProperty; import org.eclipse.gef.common.beans.property.SimpleSetMultimapProperty; import org.eclipse.gef.common.collections.CollectionUtils; import org.eclipse.gef.common.collections.ObservableSetMultimap; import org.eclipse.gef.common.tests.ObservableSetMultimapTests.InvalidationExpector; import org.eclipse.gef.common.tests.ObservableSetMultimapTests.SetMultimapChangeExpector; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.inject.Provider; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; @RunWith(Parameterized.class) public class SetMultimapPropertyTests { protected static class ChangeExpector<K, V> implements ChangeListener<ObservableSetMultimap<K, V>> { private ObservableValue<ObservableSetMultimap<K, V>> source; private LinkedList<ObservableSetMultimap<K, V>> oldValueQueue = new LinkedList<>(); private LinkedList<ObservableSetMultimap<K, V>> newValueQueue = new LinkedList<>(); public ChangeExpector( ObservableValue<ObservableSetMultimap<K, V>> source) { this.source = source; } public void addExpectation(ObservableSetMultimap<K, V> oldValue, ObservableSetMultimap<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 ObservableSetMultimap<K, V>> observable, ObservableSetMultimap<K, V> oldValue, ObservableSetMultimap<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."); } } } @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { new Provider<SetMultimapProperty<Integer, String>>() { @Override public SetMultimapProperty<Integer, String> get() { // test SimpleSetMultimapProperty, which is the // 'default' implementation of the related // ObservableValue. return new SimpleSetMultimapProperty<>(CollectionUtils .<Integer, String> observableHashMultimap()); } } }, { new Provider<SetMultimapProperty<Integer, String>>() { @Override public SetMultimapProperty<Integer, String> get() { // test ReadOnlySetMultimapWrapper, which is the // 'default' implementation of the related // read-only property support. return new ReadOnlySetMultimapWrapper<>(CollectionUtils .<Integer, String> observableHashMultimap()); } } } }); } private Provider<SetMultimapProperty<Integer, String>> propertyProvider; public SetMultimapPropertyTests( Provider<SetMultimapProperty<Integer, String>> propertyProvider) { this.propertyProvider = propertyProvider; } @Test public void bidirectionalBinding() { SetMultimapProperty<Integer, String> property1 = propertyProvider.get(); SetMultimapProperty<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 ObservableSetMultimap<Integer, String> newValue = CollectionUtils .observableHashMultimap(); 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 = CollectionUtils.observableHashMultimap(); newValue.put(2, "2-1"); property2.set(newValue); assertEquals(property1, property2); assertEquals(newValue, property1.get()); assertEquals(newValue, property2.get()); // unbind (ensure values are no longer synchronized) property2.unbindBidirectional(property1); assertFalse(property1.isBound()); assertFalse(property2.isBound()); newValue = CollectionUtils.observableHashMultimap(); newValue.put(3, "3-1"); property1.set(newValue); assertNotEquals(property1, property2); assertEquals(newValue, property1.get()); assertNotEquals(newValue, property2.get()); // 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) { // exptected } // unbind from itself (yields IAE) try { property2.unbindBidirectional(property2); fail("Expected IllegalArgumentException because binding to itself is not valid."); } catch (IllegalArgumentException e) { // expected } } /** * Test the bidirectional content bindings as offered by * {@link ReadOnlySetMultimapProperty}. */ @Test public void bidirectionalContentBinding() { SetMultimapProperty<Integer, String> property1 = propertyProvider.get(); SetMultimapProperty<Integer, String> property2 = propertyProvider.get(); ObservableSetMultimap<Integer, String> backupMap = CollectionUtils .observableHashMultimap(); property1.bindContentBidirectional(property2); // XXX: According to JavaFX contract, a content binding does not lead to // the properties being regarded as being bound assertFalse(property1.isBound()); assertFalse(property2.isBound()); // put property1.put(1, "1-1"); backupMap.put(1, "1-1"); check(property1, property2, backupMap); // putAll - single key property1.putAll(1, Sets.newHashSet("1-2", "1-3")); backupMap.putAll(1, Sets.newHashSet("1-2", "1-3")); check(property1, property2, backupMap); // putAll - multiple keys SetMultimap<Integer, String> toAdd = HashMultimap.create(); toAdd.putAll(2, Sets.newHashSet("2-1", "2-2")); toAdd.putAll(3, Sets.newHashSet("3-1", "3-2")); property1.putAll(toAdd); backupMap.putAll(toAdd); check(property1, property2, backupMap); // remove property1.remove(1, "1-1"); backupMap.remove(1, "1-1"); check(property1, property2, backupMap); // remove all - multiple keys property1.removeAll(2); backupMap.removeAll(2); check(property1, property2, backupMap); // replace values property1.replaceValues(3, Sets.newHashSet("3-3", "3-4")); backupMap.replaceValues(3, Sets.newHashSet("3-3", "3-4")); check(property1, property2, backupMap); // clear property1.clear(); backupMap.clear(); check(property1, property2, backupMap); // put property2.put(1, "1-1"); backupMap.put(1, "1-1"); check(property1, property2, backupMap); // putAll - single key property2.putAll(1, Sets.newHashSet("1-2", "1-3")); backupMap.putAll(1, Sets.newHashSet("1-2", "1-3")); check(property1, property2, backupMap); // putAll - multiple keys toAdd = HashMultimap.create(); toAdd.putAll(2, Sets.newHashSet("2-1", "2-2")); toAdd.putAll(3, Sets.newHashSet("3-1", "3-2")); property2.putAll(toAdd); backupMap.putAll(toAdd); check(property1, property2, backupMap); // remove property2.remove(1, "1-1"); backupMap.remove(1, "1-1"); check(property1, property2, backupMap); // remove all - multiple keys property2.removeAll(2); backupMap.removeAll(2); check(property1, property2, backupMap); // replace values property2.replaceValues(3, Sets.newHashSet("3-3", "3-4")); backupMap.replaceValues(3, Sets.newHashSet("3-3", "3-4")); check(property1, property2, backupMap); // clear property2.clear(); backupMap.clear(); check(property1, property2, backupMap); // unbind property2, ensure values are no longer synchronized property2.unbindContentBidirectional(property1); property1.put(1, "1-1"); assertNotEquals(property2.get(), property1.get()); assertNotEquals(property2.sizeProperty().get(), property1.sizeProperty().get()); assertNotEquals(property2.emptyProperty().get(), property1.emptyProperty().get()); // unbind property2 from null (yields NPE) try { property2.unbindContentBidirectional(null); fail("Expected NullPointerException."); } catch (NullPointerException e) { assertEquals("Cannot bind to null value.", e.getMessage()); } // unbind property2 from itself (yields IAE) try { property2.unbindContentBidirectional(property2); fail("Expected IllegalArgumentException."); } catch (IllegalArgumentException e) { assertEquals("Cannot bind source to itself.", e.getMessage()); } // bind property2 to null (yields NPE) try { property2.bindContentBidirectional(null); fail("Expected NullPointerException."); } catch (NullPointerException e) { assertEquals("Cannot bind to null value.", e.getMessage()); } // bind property2 to itself (yields IAE) try { property2.bindContentBidirectional(property2); fail("Expected IllegalArgumentException."); } catch (IllegalArgumentException e) { assertEquals("Cannot bind source to itself.", e.getMessage()); } } /** * Check change notifications for observed value changes are properly fired. */ @Test public void changeNotifications() { SetMultimapProperty<Integer, String> property = propertyProvider.get(); // initialize property property.putAll(1, Sets.newHashSet("1-1", "1-2", "1-3")); property.putAll(2, Sets.newHashSet("2-1", "2-2", "2-3")); // register listener InvalidationExpector invalidationListener = new InvalidationExpector(); SetMultimapChangeExpector<Integer, String> setMultimapChangeListener = new SetMultimapChangeExpector<>( property); ChangeExpector<Integer, String> changeListener = new ChangeExpector<>( property); property.addListener(invalidationListener); property.addListener(setMultimapChangeListener); property.addListener(changeListener); // change property value (disjoint values) ObservableSetMultimap<Integer, String> newValue = CollectionUtils .observableHashMultimap(); newValue.putAll(3, Sets.newHashSet("3-1", "3-2", "3-3")); newValue.putAll(4, Sets.newHashSet("4-1", "4-2", "4-3")); invalidationListener.expect(1); changeListener.addExpectation(property.get(), newValue); setMultimapChangeListener.addAtomicExpectation(); setMultimapChangeListener.addElementaryExpectation(1, Sets.newHashSet("1-1", "1-2", "1-3"), Collections.<String> emptySet()); setMultimapChangeListener.addElementaryExpectation(2, Sets.newHashSet("2-1", "2-2", "2-3"), Collections.<String> emptySet()); setMultimapChangeListener.addElementaryExpectation(3, Collections.<String> emptySet(), Sets.newHashSet("3-1", "3-2", "3-3")); setMultimapChangeListener.addElementaryExpectation(4, Collections.<String> emptySet(), Sets.newHashSet("4-1", "4-2", "4-3")); property.set(newValue); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); // change property value (overlapping values) newValue = CollectionUtils.observableHashMultimap(); newValue.putAll(1, Sets.newHashSet("1-1", "1-2", "1-3")); newValue.putAll(3, Sets.newHashSet("3-2", "3-4")); newValue.putAll(4, Sets.newHashSet("4-1", "4-2", "4-3")); invalidationListener.expect(1); changeListener.addExpectation(property.get(), newValue); setMultimapChangeListener.addAtomicExpectation(); setMultimapChangeListener.addElementaryExpectation(3, Sets.newHashSet("3-1", "3-3"), Sets.newHashSet("3-4")); setMultimapChangeListener.addElementaryExpectation(1, Collections.<String> emptySet(), Sets.newHashSet("1-1", "1-2", "1-3")); property.set(newValue); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); // change property value (change to null) invalidationListener.expect(1); changeListener.addExpectation(property.get(), null); setMultimapChangeListener.addAtomicExpectation(); setMultimapChangeListener.addElementaryExpectation(1, Sets.newHashSet("1-1", "1-2", "1-3"), Collections.<String> emptySet()); setMultimapChangeListener.addElementaryExpectation(3, Sets.newHashSet("3-2", "3-4"), Collections.<String> emptySet()); setMultimapChangeListener.addElementaryExpectation(4, Sets.newHashSet("4-1", "4-2", "4-3"), Collections.<String> emptySet()); property.set(null); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); // set to null again (no expectation) property.set(null); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); // change property value (change from null) newValue = CollectionUtils.observableHashMultimap(); newValue.putAll(1, Sets.newHashSet("1-1", "1-2", "1-3")); invalidationListener.expect(1); changeListener.addExpectation(null, newValue); setMultimapChangeListener.addAtomicExpectation(); setMultimapChangeListener.addElementaryExpectation(1, Collections.<String> emptySet(), Sets.newHashSet("1-1", "1-2", "1-3")); property.set(newValue); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); // set to identical value (no notifications expected) property.set(newValue); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); // set to equal value (no list change notification expected) newValue = CollectionUtils.observableHashMultimap(); newValue.putAll(1, Sets.newHashSet("1-1", "1-2", "1-3")); invalidationListener.expect(1); changeListener.addExpectation(property.get(), newValue); property.set(newValue); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); // change observed value (only invalidation and list change expected) invalidationListener.expect(1); setMultimapChangeListener.addAtomicExpectation(); setMultimapChangeListener.addElementaryExpectation(1, Sets.newHashSet("1-1", "1-2", "1-3"), Collections.<String> emptySet()); property.get().removeAll(1); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); // touch observed value (don't change it) property.get().removeAll(1); invalidationListener.check(); setMultimapChangeListener.check(); changeListener.check(); } protected void check(SetMultimapProperty<Integer, String> property1, SetMultimapProperty<Integer, String> property2, ObservableSetMultimap<Integer, String> backupMap) { assertEquals(property1, property2); assertEquals(property1.get(), property2.get()); assertEquals(backupMap, property1.get()); assertEquals(backupMap, property2.get()); assertEquals(backupMap.size(), property1.sizeProperty().get()); assertEquals(backupMap.size(), property2.sizeProperty().get()); assertEquals(backupMap.isEmpty(), property1.emptyProperty().get()); assertEquals(backupMap.isEmpty(), property2.emptyProperty().get()); } @Test public void unidirectionalBinding() { SetMultimapProperty<Integer, String> property1 = propertyProvider.get(); SetMultimapProperty<Integer, String> property2 = propertyProvider.get(); // bind properly property2.bind(property1); assertFalse(property1.isBound()); assertTrue(property2.isBound()); // change value of first property ObservableSetMultimap<Integer, String> newValue = CollectionUtils .observableHashMultimap(); newValue.put(1, "1-1"); property1.set(newValue); assertEquals(newValue, property1.get()); assertEquals(newValue, property2.get()); assertEquals(property1, property2); // set value on second (bound) property (yields IAE) try { property2.set( CollectionUtils.<Integer, String> observableHashMultimap()); fail("Expected IllegalArgumentException because property is bound."); } catch (IllegalArgumentException e) { assertEquals("A bound value cannot be set.", e.getMessage()); } // unbind property2.unbind(); assertFalse(property1.isBound()); assertFalse(property2.isBound()); // change value after binding has been removed newValue = CollectionUtils.observableHashMultimap(); newValue.put(3, "3-1"); property1.set(newValue); assertNotEquals(property1, property2); assertEquals(newValue, property1.get()); assertNotEquals(newValue, property2.get()); // bind on null (yields NPE) try { property2.bind(null); fail("Expected NullPointerException because binding to null is not valid."); } catch (NullPointerException e) { assertEquals("Cannot bind to null.", e.getMessage()); } // according to JavaFX, binding on itself does not yield an IAE here } /** * Test the unidirectional content bindings as offered by * {@link ReadOnlySetMultimapProperty}. */ @Test public void unidirectionalContentBinding() { SetMultimapProperty<Integer, String> property1 = propertyProvider.get(); SetMultimapProperty<Integer, String> property2 = propertyProvider.get(); ObservableSetMultimap<Integer, String> backupMap = CollectionUtils .observableHashMultimap(); property2.bindContent(property1); // XXX: According to JavaFX contract, content binding does not lead to // the properties being regarded as being bound. assertFalse(property1.isBound()); assertFalse(property2.isBound()); // put property1.put(1, "1-1"); backupMap.put(1, "1-1"); check(property1, property2, backupMap); // putAll - single key property1.putAll(1, Sets.newHashSet("1-2", "1-3")); backupMap.putAll(1, Sets.newHashSet("1-2", "1-3")); check(property1, property2, backupMap); // putAll - multiple keys SetMultimap<Integer, String> toAdd = HashMultimap.create(); toAdd.putAll(2, Sets.newHashSet("2-1", "2-2")); toAdd.putAll(3, Sets.newHashSet("3-1", "3-2")); property1.putAll(toAdd); backupMap.putAll(toAdd); check(property1, property2, backupMap); // remove property1.remove(1, "1-1"); backupMap.remove(1, "1-1"); check(property1, property2, backupMap); // remove all - multiple keys property1.removeAll(2); backupMap.removeAll(2); check(property1, property2, backupMap); // replace values property1.replaceValues(3, Sets.newHashSet("3-3", "3-4")); backupMap.replaceValues(3, Sets.newHashSet("3-3", "3-4")); check(property1, property2, backupMap); // clear property1.clear(); backupMap.clear(); check(property1, property2, backupMap); // unbind property2, ensure values are no longer synchronized property2.unbindContent(property1); property1.put(1, "1-1"); assertNotEquals(property2.get(), property1.get()); assertNotEquals(property2.sizeProperty().get(), property1.sizeProperty().get()); assertNotEquals(property2.emptyProperty().get(), property1.emptyProperty().get()); // unbind property2 from null (yields NPE) try { property2.unbindContent(null); fail("Expected NullPointerException."); } catch (NullPointerException e) { assertEquals("Cannot unbind from null value.", e.getMessage()); } // unbind property2 from itself (yields IAE) try { property2.unbindContent(property2); fail("Expected IllegalArgumentException."); } catch (IllegalArgumentException e) { assertEquals("Cannot unbind source to itself.", e.getMessage()); } // bind property2 to null (yields NPE) try { property2.bindContent(null); fail("Expected NullPointerException."); } catch (NullPointerException e) { assertEquals("Cannot bind to null value.", e.getMessage()); } // bind property2 to itself (yields IAE) try { property2.bindContent(property2); fail("Expected IllegalArgumentException."); } catch (IllegalArgumentException e) { assertEquals("Cannot bind source to itself.", e.getMessage()); } } }