/****************************************************************************** * 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.Iterator; import java.util.List; import javafx.beans.InvalidationListener; import javafx.collections.MapChangeListener; import javafx.collections.MapChangeListener.Change; import javafx.collections.ObservableMap; /** * A utility class to support change notifications for an {@link ObservableMap} * , replacing the JavaFX-internal {@code MapChangeListener} helper class. * * @author anyssen * * @param <K> * The key type of the {@link ObservableMap}. * @param <V> * The value type of the {@link ObservableMap}. * */ public class MapListenerHelperEx<K, V> { /** * A simple implementation of an * {@link javafx.collections.MapChangeListener.Change}. * * @author anyssen * * @param <K> * The key type of the source {@link ObservableMap}. * @param <V> * The value type of the source {@link ObservableMap}. * */ public static class AtomicChange<K, V> extends MapChangeListener.Change<K, V> { private K key; private V removedValue; private V addedValue; /** * Creates a new {@link MapListenerHelperEx.AtomicChange} that * represents a change comprising a single elementary sub-change. * * @param source * The source {@link ObservableMap} from which the change * originated. * @param key * The key to which the change is related. * @param removedValue * The value that was removed by this change or * <code>null</code> if no value was removed. * @param addedValue * The value that was added by this change or * <code>null</code> if no value was added. */ public AtomicChange(ObservableMap<K, V> source, K key, V removedValue, V addedValue) { super(source); this.key = key; this.removedValue = removedValue; this.addedValue = addedValue; } /** * Creates a new {@link MapListenerHelperEx.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 ObservableMap} to re-fire change events of their wrapped * {@link ObservableMap} with themselves as source. * * @param source * The new source {@link ObservableMap}. * @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. */ public AtomicChange(ObservableMap<K, V> source, MapChangeListener.Change<? extends K, ? extends V> change) { super(source); this.key = change.getKey(); this.addedValue = change.getValueAdded(); this.removedValue = change.getValueRemoved(); } @Override public K getKey() { return key; } @Override public V getValueAdded() { return addedValue; } @Override public V getValueRemoved() { return removedValue; } @Override public String toString() { if (wasAdded()) { if (wasRemoved()) { return "Replaced " + removedValue + " by " + addedValue + " for key " + key + "."; } return "Added " + addedValue + " for key " + key + "."; } else { return "Removed " + removedValue + " for key " + key + "."; } } @Override public boolean wasAdded() { return addedValue != null; } @Override public boolean wasRemoved() { return removedValue != null; } } private List<InvalidationListener> invalidationListeners = null; private boolean lockInvalidationListeners; private boolean lockMapChangeListeners; private List<MapChangeListener<? super K, ? super V>> mapChangeListeners = null; private ObservableMap<K, V> source; /** * Constructs a new {@link MapListenerHelperEx} for the given source * {@link ObservableMap}. * * @param source * The {@link ObservableMap} to use as source in change * notifications. */ public MapListenerHelperEx(ObservableMap<K, V> source) { this.source = source; } /** * Adds a new {@link InvalidationListener} to this * {@link MapListenerHelperEx}. 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 MapChangeListener} to this {@link MapListenerHelperEx}. * 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(MapChangeListener<? super K, ? super V> listener) { if (mapChangeListeners == null) { mapChangeListeners = 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 (lockMapChangeListeners) { mapChangeListeners = new ArrayList<>(mapChangeListeners); } mapChangeListeners.add(listener); } /** * Notifies all attached {@link InvalidationListener}s and * {@link MapChangeListener}s about the change. * * @param change * The change to notify listeners about. */ public void fireValueChangedEvent( MapChangeListener.Change<? extends K, ? extends V> change) { notifyInvalidationListeners(); if (change != null) { notifyMapChangeListeners(change); } } /** * Returns the source {@link ObservableMap} this {@link MapListenerHelperEx} * is bound to, which is used in change notifications. * * @return The source {@link ObservableMap}. */ protected ObservableMap<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 MapChangeListener}s about the related * change. * * @param change * The applied change. */ protected void notifyMapChangeListeners( Change<? extends K, ? extends V> change) { if (mapChangeListeners != null && !mapChangeListeners.isEmpty()) { // if (lockMapChangeListeners) { // throw new IllegalStateException("Re-entrant map change!"); // } try { lockMapChangeListeners = true; for (MapChangeListener<? super K, ? super V> l : mapChangeListeners) { try { l.onChanged(change); } catch (Exception e) { // System.out.println("Exception in listener: " + // e.getMessage() + ", cause=" + e.getCause()); Thread.currentThread().getUncaughtExceptionHandler() .uncaughtException(Thread.currentThread(), e); } } } finally { lockMapChangeListeners = false; } } } /** * Removes the given {@link InvalidationListener} from this * {@link MapListenerHelperEx}. 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 MapChangeListener} from this * {@link MapListenerHelperEx}. If its was registered more than once, * removes one occurrence. * * @param listener * The listener to remove. */ public void removeListener( MapChangeListener<? super K, ? super V> listener) { if (mapChangeListeners == 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 (lockMapChangeListeners) { mapChangeListeners = new ArrayList<>(mapChangeListeners); } // 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<MapChangeListener<? super K, ? super V>> iterator = mapChangeListeners .iterator(); iterator.hasNext();) { if (iterator.next().equals(listener)) { iterator.remove(); break; } } if (mapChangeListeners.isEmpty()) { mapChangeListeners = null; } } }