All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.binjr.common.javafx.bindings.BindingManager Maven / Gradle / Ivy

There is a newer version: 3.20.1
Show newest version
/*
 *    Copyright 2017-2021 Frederic Thevenet
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package eu.binjr.common.javafx.bindings;

import eu.binjr.common.logging.Logger;
import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;

/**
 * A class that provide methods to centralize and register the attachment of listeners
 * and bindings onto {@link javafx.beans.Observable} instances.
 * 

* This makes it possible to remove all listeners and bindings attached to to * registered {@link javafx.beans.Observable} instances in a determinist fashion (on invoking {@code close()}) * and helps alleviate the potential for references leaks in some scenarios. *

* * @author Frederic Thevenet */ @SuppressWarnings("rawtypes") public class BindingManager implements AutoCloseable { private static final Logger logger = Logger.create(BindingManager.class); private final Map> changeListeners = Collections.synchronizedMap(new WeakHashMap<>()); private final Map> invalidationListeners = Collections.synchronizedMap(new WeakHashMap<>()); private final Map> listChangeListeners = Collections.synchronizedMap(new WeakHashMap<>()); private final Map> listInvalidationListeners = Collections.synchronizedMap(new WeakHashMap<>()); private final Map> setChangeListeners = Collections.synchronizedMap(new WeakHashMap<>()); private final Map> setInvalidationListeners= Collections.synchronizedMap(new WeakHashMap<>()); private final Map, ObservableValue> boundProperties = Collections.synchronizedMap(new WeakHashMap<>()); private final Map, Property> bidirectionallyBoundProperties = Collections.synchronizedMap(new WeakHashMap<>()); private final List> registeredHandlers = Collections.synchronizedList(new ArrayList<>()); private final AtomicBoolean closed = new AtomicBoolean(false); /** * Binds the specified {@link ObservableValue} onto the specified {@link Property} and registers the resulting binding. * * @param property the {@link Property} to bind * @param binding the {@link ObservableValue} to bind to the {@link Property} * @param the type of {@link Property} * @param the type of {@link ObservableValue} */ public void bind(Property property, ObservableValue binding) { Objects.requireNonNull(property, "property parameter cannot be null"); Objects.requireNonNull(binding, "binding parameter cannot be null"); logger.trace(() -> "Binding " + binding.toString() + " to " + property.toString()); property.bind(binding); boundProperties.put(property, binding); } /** * Unbinds all registered bindings */ public void unbindAll() { boundProperties.forEach((property, binding) -> { logger.trace(() -> "Unbinding property " + property.toString()); property.unbind(); }); boundProperties.clear(); bidirectionallyBoundProperties.forEach((property, binding) -> { logger.trace(() -> "Unbinding property " + property.toString() + " from " + binding.toString()); property.unbindBidirectional(binding); }); bidirectionallyBoundProperties.clear(); } public void bindBidirectional(Property property, Property binding) { Objects.requireNonNull(property, "property parameter cannot be null"); Objects.requireNonNull(binding, "binding parameter cannot be null"); logger.trace(() -> "Binding " + binding.toString() + " to " + property.toString()); property.bindBidirectional(binding); bidirectionallyBoundProperties.put(property, binding); } public void unbindBidirectionnal(Property property, Property binding) { Objects.requireNonNull(property, "property parameter cannot be null"); Objects.requireNonNull(binding, "binding parameter cannot be null"); logger.trace(() -> "Unbinding " + binding.toString() + " from " + property.toString()); property.unbindBidirectional(binding); bidirectionallyBoundProperties.remove(property, binding); } /** * Attach a {@link ChangeListener} to an {@link ObservableValue} and registers the resulting binding. * * @param observable the {@link ObservableValue} to attach the listener to. * @param listener the {@link ChangeListener} to attach */ public void attachListener(ObservableValue observable, ChangeListener listener) { register(observable, listener, changeListeners, ObservableValue::addListener); } /** * Attach a {@link InvalidationListener} to an {@link ObservableValue} and registers the resulting binding. * * @param observable the {@link ObservableValue} to attach the listener to. * @param listener the {@link InvalidationListener} to attach */ public void attachListener(ObservableValue observable, InvalidationListener listener) { register(observable, listener, invalidationListeners, ObservableValue::addListener); } /** * Attach a {@link ListChangeListener} to an {@link ObservableList} and registers the resulting binding. * * @param observable the {@link ObservableList} to attach the listener to. * @param listener the {@link ListChangeListener} to attach */ public void attachListener(ObservableList observable, ListChangeListener listener) { register(observable, listener, listChangeListeners, ObservableList::addListener); } public void attachListener(ObservableList observable, InvalidationListener listener) { register(observable, listener, listInvalidationListeners, ObservableList::addListener); } public void attachListener(ObservableSet observable, SetChangeListener listener){ register(observable, listener, setChangeListeners, ObservableSet::addListener); } public void attachListener(ObservableSet observable, InvalidationListener listener){ register(observable, listener, setInvalidationListeners, ObservableSet::addListener); } public void detachListener(ObservableSet observable, SetChangeListener listener) { unregister(observable, listener, setChangeListeners, ObservableSet::removeListener); } public void detachListener(ObservableSet observable, InvalidationListener listener) { unregister(observable, listener, setInvalidationListeners, ObservableSet::removeListener); } /** * Remove a specific {@link ChangeListener} from an {@link ObservableValue}. * * @param observable the {@link ObservableValue} to remove the listener from. * @param listener the {@link ChangeListener} to remove */ public void detachListener(ObservableValue observable, ChangeListener listener) { unregister(observable, listener, changeListeners, ObservableValue::removeListener); } /** * Remove a specific {@link InvalidationListener} from an {@link ObservableValue}. * * @param observable the {@link ObservableValue} to remove the listener from. * @param listener the {@link InvalidationListener} to remove */ public void detachListener(ObservableValue observable, InvalidationListener listener) { unregister(observable, listener, invalidationListeners, ObservableValue::removeListener); } /** * Remove a specific {@link ListChangeListener} from an {@link ObservableList}. * * @param observable the {@link ObservableList} to remove the listener from. * @param listener the {@link ListChangeListener} to remove */ public void detachListener(ObservableList observable, ListChangeListener listener) { unregister(observable, listener, listChangeListeners, ObservableList::removeListener); } public void detachListener(ObservableList observable, InvalidationListener listener) { unregister(observable, listener, listInvalidationListeners, ObservableList::removeListener); } /** * Remove all {@link InvalidationListener} from an {@link ObservableValue}. * * @param observable the {@link ObservableValue} to remove all listeners from. */ public void detachAllInvalidationListeners(ObservableValue observable) { unregister(observable, invalidationListeners, ObservableValue::removeListener); } /** * Remove all {@link ChangeListener} from an {@link ObservableValue}. * * @param observable the {@link ObservableValue} to remove all listeners from. */ public void detachAllChangeListeners(ObservableValue observable) { unregister(observable, changeListeners, ObservableValue::removeListener); } /** * Remove all {@link ListChangeListener} from an {@link ObservableList}. * * @param observable the {@link ObservableList} to remove all listeners from. */ public void detachAllListChangeListeners(ObservableList observable) { unregister(observable, listChangeListeners, ObservableList::removeListener); } public void detachAllInvalidationListeners(ObservableList observable) { unregister(observable, listInvalidationListeners, ObservableList::removeListener); } @Override public synchronized void close() { if (closed.compareAndSet(false, true)) { try { unregisterAll(listChangeListeners, ObservableList::removeListener); unregisterAll(listInvalidationListeners, ObservableList::removeListener); unregisterAll(invalidationListeners, ObservableValue::removeListener); unregisterAll(changeListeners, ObservableValue::removeListener); unregisterAll(setChangeListeners, ObservableSet::removeListener); unregisterAll(setInvalidationListeners, ObservableSet::removeListener); unbindAll(); // Release strong refs to registered event handlers, so that their // weak counterpart may be collected. registeredHandlers.clear(); } catch (Exception e) { logger.warn("An error occurred while closing BindingManager instance", e); } } } public synchronized void suspend() { visitMap(listChangeListeners, ObservableList::removeListener); visitMap(listInvalidationListeners, ObservableList::removeListener); visitMap(invalidationListeners, ObservableValue::removeListener); visitMap(changeListeners, ObservableValue::removeListener); visitMap(setChangeListeners, ObservableSet::removeListener); visitMap(setInvalidationListeners, ObservableSet::removeListener); boundProperties.keySet().forEach(Property::unbind); } public synchronized void resume() { visitMap(listChangeListeners, ObservableList::addListener); visitMap(listInvalidationListeners, ObservableList::addListener); visitMap(invalidationListeners, ObservableValue::addListener); visitMap(changeListeners, ObservableValue::addListener); visitMap(setChangeListeners, ObservableSet::addListener); visitMap(setInvalidationListeners, ObservableSet::addListener); boundProperties.forEach(Property::bind); } public synchronized void suspend(Property... properties) { for (var p : properties) { if (!boundProperties.containsKey(p)) { throw new IllegalArgumentException("Property " + p.getName() + " is not managed by this instance of BindingManager"); } p.unbind(); } } public synchronized void resume(Property... properties) { for (var p : properties) { if (!boundProperties.containsKey(p)) { throw new IllegalArgumentException("Property " + p.getName() + " is not managed by this instance of BindingManager"); } p.bind(boundProperties.get(p)); } } public WeakEventHandler registerHandler(EventHandler handler) { // Store strong ref to handler, so it doesn't get collected prematurely. registeredHandlers.add(handler); // wrap in WeakEventHandler return new WeakEventHandler(handler); } private void register(T observable, U listener, Map> map, BiConsumer attachAction) { Objects.requireNonNull(observable, "observable parameter cannot be null"); Objects.requireNonNull(listener, "listener parameter cannot be null"); Objects.requireNonNull(map, "map parameter cannot be null"); Objects.requireNonNull(attachAction, "attachAction parameter cannot be null"); map.computeIfAbsent(observable, p -> new ArrayList<>()).add(listener); logger.trace(() -> "Attaching listener " + listener.toString() + " to observable " + observable.toString()); attachAction.accept(observable, listener); } private void unregister(T key, U value, Map> map, BiConsumer unregisterAction) { Objects.requireNonNull(key, "key parameter cannot be null"); Objects.requireNonNull(value, "value parameter cannot be null"); Objects.requireNonNull(map, "map parameter cannot be null"); Objects.requireNonNull(unregisterAction, "unregisterAction parameter cannot be null"); List listeners = map.get(key); if (listeners == null) { logger.debug(() -> "Object " + key.toString() + " is not managed by this BindingManager instance"); return; } listeners.stream().filter(l -> l.equals(value)).findFirst().ifPresent(found -> map.get(key).remove(found)); logger.trace(() -> "Unregistering " + value.toString() + " from " + key.toString()); unregisterAction.accept(key, value); } private void unregister(T key, Map> map, BiConsumer unregisterAction) { Objects.requireNonNull(key, "key paramater cannot be null"); Objects.requireNonNull(map, "map parameter cannot be null"); Objects.requireNonNull(unregisterAction, "unregisterAction parameter cannot be null"); List l = map.get(key); if (l == null) { logger.debug(() -> "Object " + key.toString() + " is not managed by this BindingManager instance"); return; } l.forEach(value -> { logger.trace(() -> "Unregistering " + value.toString() + " from " + key.toString()); unregisterAction.accept(key, value); }); map.remove(key); } private void unregisterAll(Map> map, BiConsumer unregisterAction) { Objects.requireNonNull(map, "map parameter cannot be null"); Objects.requireNonNull(unregisterAction, "unregisterAction parameter cannot be null"); map.forEach((k, vList) -> { vList.forEach(v -> { logger.trace(() -> "Unregistering " + v.toString() + " from " + k.toString()); unregisterAction.accept(k, v); }); }); map.clear(); } private void visitMap(Map> map, BiConsumer action) { Objects.requireNonNull(map, "map parameter cannot be null"); Objects.requireNonNull(action, "action parameter cannot be null"); map.forEach((observable, listeners) -> listeners.forEach(listener -> { logger.trace(() -> "visiting key " + listener.toString() + " value " + observable.toString()); action.accept(observable, listener); })); } }