/****************************************************************************** * 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.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.eclipse.gef.common.collections.SetMultimapChangeListener.Change; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimaps; import com.google.common.collect.SetMultimap; import javafx.beans.InvalidationListener; /** * 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 SetMultimapListenerHelper<K, V> { /** * A simple implementation of an {@link SetMultimapChangeListener.Change}. * * @author anyssen * * @param <K> * The key type of the source {@link ObservableSetMultimap}. * @param <V> * The value type of the source {@link ObservableSetMultimap}. * */ public static class AtomicChange<K, V> extends SetMultimapChangeListener.Change<K, V> { private SetMultimap<K, V> previousContents; private ElementarySubChange<K, V>[] elementarySubChanges; private int cursor = -1; /** * Creates a new {@link SetMultimapListenerHelper.AtomicChange} that * represents a change comprising a single elementary sub-change. * * @param source * The source {@link ObservableSetMultimap} from which the * change originated. * @param previousContents * The previous contents of the {@link ObservableSetMultimap} * before the change was applied. * @param elementarySubChange * The elementary sub-change that has been applied. */ @SuppressWarnings("unchecked") public AtomicChange(ObservableSetMultimap<K, V> source, SetMultimap<K, V> previousContents, ElementarySubChange<K, V> 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(ObservableSetMultimap<K, V> source, SetMultimap<K, V> previousContents, List<ElementarySubChange<K, V>> 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(ObservableSetMultimap<K, V> source, SetMultimapChangeListener.Change<? extends K, ? extends V> change) { super(source); // copy previous contents this.previousContents = HashMultimap .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<K, V>> elementarySubChanges = new ArrayList<>(); while (change.next()) { elementarySubChanges.add(new ElementarySubChange<K, V>( change.getKey(), change.getValuesRemoved(), change.getValuesAdded())); } 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 K getKey() { checkCursor(); return elementarySubChanges[cursor].getKey(); } @Override public SetMultimap<K, V> getPreviousContents() { return Multimaps.unmodifiableSetMultimap(previousContents); } @Override public Set<V> getValuesAdded() { checkCursor(); return elementarySubChanges[cursor].getValuesAdded(); } @Override public Set<V> getValuesRemoved() { checkCursor(); return elementarySubChanges[cursor].getValuesRemoved(); } @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(); } @Override public boolean wasAdded() { checkCursor(); return elementarySubChanges[cursor].wasAdded(); } @Override public boolean wasRemoved() { checkCursor(); return elementarySubChanges[cursor].wasRemoved(); } } /** * An elementary change related to a single key of a * {@link ObservableSetMultimap}. . * * @author anyssen * * @param <K> * The key type of the {@link ObservableSetMultimap}. * @param <V> * The value type of the {@link ObservableSetMultimap}. */ public static class ElementarySubChange<K, V> { private K key = null; private Set<V> removedValues; private Set<V> addedValues; /** * Constructs a new * {@link SetMultimapListenerHelper.ElementarySubChange} with the given * values. * * @param key * The key to which the change is related. * @param removedValues * The values removed by the change. * @param addedValues * The values added by the change. */ public ElementarySubChange(K key, Set<? extends V> removedValues, Set<? extends V> addedValues) { this.key = key; this.removedValues = new HashSet<>(removedValues); this.addedValues = new HashSet<>(addedValues); } /** * Returns the key that was modified in this elementary sub-change, i.e. * for which values were added or removed. * * @return The key this elementary sub-change is related to. */ public K getKey() { return key; } /** * Returns the values added by this elementary sub-change. * * @return The values that were added by this elementary sub-change, if * any. Will return an empty set in case no elements were added. */ public Set<V> getValuesAdded() { return addedValues; } /** * Returns the values removed by this elementary sub-change. * * @return The values that were removed by this elementary sub-change, * if any. Will return an empty set in case no elements were * removed. */ public Set<V> getValuesRemoved() { return removedValues; } @Override public String toString() { StringBuilder builder = new StringBuilder(); if (wasAdded()) { if (wasRemoved()) { builder.append("Replaced ").append(removedValues) .append(" by ").append(addedValues); } else { builder.append("Added ").append(addedValues); } } else { builder.append("Removed ").append(removedValues); } builder.append(" for key ").append(key).append("."); return builder.toString(); } /** * Indicates whether values were added by this elementary sub-change. * * @return <code>true</code> if values were added, <code>false</code> * otherwise. */ public boolean wasAdded() { return !addedValues.isEmpty(); } /** * Indicates whether values were removed by this elementary sub-change. * * @return <code>true</code> if values were removed, <code>false</code> * otherwise. */ public boolean wasRemoved() { return !removedValues.isEmpty(); } } private List<InvalidationListener> invalidationListeners = null; private List<SetMultimapChangeListener<? super K, ? super V>> setMultimapChangeListeners = null; private ObservableSetMultimap<K, V> source; private boolean lockInvalidationListeners; private boolean lockSetMultimapChangeListeners; /** * Constructs a new {@link SetMultimapListenerHelper} for the given source * {@link ObservableSetMultimap}. * * @param source * The {@link ObservableSetMultimap} to use as source in change * notifications. */ public SetMultimapListenerHelper(ObservableSetMultimap<K, V> source) { this.source = source; } /** * Adds a new {@link InvalidationListener} to this * {@link SetMultimapListenerHelper}. 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 SetMultimapListenerHelper}. 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( SetMultimapChangeListener<? super K, ? super V> listener) { if (setMultimapChangeListeners == null) { setMultimapChangeListeners = 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 (lockSetMultimapChangeListeners) { setMultimapChangeListeners = new ArrayList<>( setMultimapChangeListeners); } setMultimapChangeListeners.add(listener); } /** * Notifies all attached {@link InvalidationListener}s and * {@link SetMultimapChangeListener}s about the change. * * @param change * The change to notify listeners about. */ public void fireValueChangedEvent( SetMultimapChangeListener.Change<? extends K, ? extends V> change) { notifyInvalidationListeners(); if (change != null) { notifySetMultimapChangeListeners(change); } } /** * Returns the source {@link ObservableSetMultimap} this * {@link SetMultimapListenerHelper} is bound to, which is used in change * notifications. * * @return The source {@link ObservableSetMultimap}. */ protected ObservableSetMultimap<K, V> 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 SetMultimapChangeListener}s about the * related change. * * @param change * The applied change. */ protected void notifySetMultimapChangeListeners( Change<? extends K, ? extends V> change) { if (setMultimapChangeListeners != null) { try { lockSetMultimapChangeListeners = true; for (SetMultimapChangeListener<? super K, ? super V> l : setMultimapChangeListeners) { change.reset(); try { l.onChanged(change); } catch (Exception e) { Thread.currentThread().getUncaughtExceptionHandler() .uncaughtException(Thread.currentThread(), e); } } } finally { lockSetMultimapChangeListeners = false; } } } /** * Removes the given {@link InvalidationListener} from this * {@link SetMultimapListenerHelper}. 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 SetMultimapChangeListener} from this * {@link SetMultimapListenerHelper}. If its was registered more than once, * removes one occurrence. * * @param listener * The listener to remove. */ public void removeListener( SetMultimapChangeListener<? super K, ? super V> listener) { if (setMultimapChangeListeners == 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 (lockSetMultimapChangeListeners) { setMultimapChangeListeners = new ArrayList<>( setMultimapChangeListeners); } // 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<SetMultimapChangeListener<? super K, ? super V>> iterator = setMultimapChangeListeners .iterator(); iterator.hasNext();) { if (iterator.next().equals(listener)) { iterator.remove(); break; } } if (setMultimapChangeListeners.isEmpty()) { setMultimapChangeListeners = null; } } }