Created
July 8, 2022 20:59
-
-
Save hjohn/f88362ea78adef96f3a54d97e2405076 to your computer and use it in GitHub Desktop.
ExpressionHelper using a different collection for large lists and different locking
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. | |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
* | |
* This code is free software; you can redistribute it and/or modify it | |
* under the terms of the GNU General Public License version 2 only, as | |
* published by the Free Software Foundation. Oracle designates this | |
* particular file as subject to the "Classpath" exception as provided | |
* by Oracle in the LICENSE file that accompanied this code. | |
* | |
* This code is distributed in the hope that it will be useful, but WITHOUT | |
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
* version 2 for more details (a copy is included in the LICENSE file that | |
* accompanied this code). | |
* | |
* You should have received a copy of the GNU General Public License version | |
* 2 along with this work; if not, write to the Free Software Foundation, | |
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
* | |
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
* or visit www.oracle.com if you need additional information or have any | |
* questions. | |
*/ | |
package com.sun.javafx.binding; | |
import java.util.Iterator; | |
import java.util.List; | |
import java.util.concurrent.atomic.AtomicBoolean; | |
import javafx.beans.InvalidationListener; | |
import javafx.beans.value.ChangeListener; | |
import javafx.beans.value.ObservableValue; | |
/** | |
* A convenience class for creating implementations of {@link javafx.beans.value.ObservableValue}. | |
* It contains all of the infrastructure support for value invalidation- and | |
* change event notification. | |
* | |
* This implementation can handle adding and removing listeners while the | |
* observers are being notified, but it is not thread-safe. | |
* | |
* | |
*/ | |
public abstract class ExpressionHelper<T> extends ExpressionHelperBase { | |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Static methods | |
public static <T> ExpressionHelper<T> addListener(ExpressionHelper<T> helper, ObservableValue<T> observable, InvalidationListener listener) { | |
if ((observable == null) || (listener == null)) { | |
throw new NullPointerException(); | |
} | |
observable.getValue(); // validate observable | |
return (helper == null)? new SingleInvalidation<T>(observable, listener) : helper.addListener(listener); | |
} | |
public static <T> ExpressionHelper<T> removeListener(ExpressionHelper<T> helper, InvalidationListener listener) { | |
if (listener == null) { | |
throw new NullPointerException(); | |
} | |
return (helper == null)? null : helper.removeListener(listener); | |
} | |
public static <T> ExpressionHelper<T> addListener(ExpressionHelper<T> helper, ObservableValue<T> observable, ChangeListener<? super T> listener) { | |
if ((observable == null) || (listener == null)) { | |
throw new NullPointerException(); | |
} | |
return (helper == null)? new SingleChange<T>(observable, listener) : helper.addListener(listener); | |
} | |
public static <T> ExpressionHelper<T> removeListener(ExpressionHelper<T> helper, ChangeListener<? super T> listener) { | |
if (listener == null) { | |
throw new NullPointerException(); | |
} | |
return (helper == null)? null : helper.removeListener(listener); | |
} | |
public static <T> void fireValueChangedEvent(ExpressionHelper<T> helper) { | |
if (helper != null) { | |
helper.fireValueChangedEvent(); | |
} | |
} | |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Common implementations | |
protected final ObservableValue<T> observable; | |
private ExpressionHelper(ObservableValue<T> observable) { | |
this.observable = observable; | |
} | |
protected abstract ExpressionHelper<T> addListener(InvalidationListener listener); | |
protected abstract ExpressionHelper<T> removeListener(InvalidationListener listener); | |
protected abstract ExpressionHelper<T> addListener(ChangeListener<? super T> listener); | |
protected abstract ExpressionHelper<T> removeListener(ChangeListener<? super T> listener); | |
protected abstract void fireValueChangedEvent(); | |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// Implementations | |
private static class SingleInvalidation<T> extends ExpressionHelper<T> { | |
private final InvalidationListener listener; | |
private SingleInvalidation(ObservableValue<T> expression, InvalidationListener listener) { | |
super(expression); | |
this.listener = listener; | |
} | |
@Override | |
protected ExpressionHelper<T> addListener(InvalidationListener listener) { | |
return new Generic<T>(observable, this.listener, listener); | |
} | |
@Override | |
protected ExpressionHelper<T> removeListener(InvalidationListener listener) { | |
return (listener.equals(this.listener))? null : this; | |
} | |
@Override | |
protected ExpressionHelper<T> addListener(ChangeListener<? super T> listener) { | |
return new Generic<T>(observable, this.listener, listener); | |
} | |
@Override | |
protected ExpressionHelper<T> removeListener(ChangeListener<? super T> listener) { | |
return this; | |
} | |
@Override | |
protected void fireValueChangedEvent() { | |
try { | |
listener.invalidated(observable); | |
} catch (Exception e) { | |
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); | |
} | |
} | |
} | |
private static class SingleChange<T> extends ExpressionHelper<T> { | |
private final ChangeListener<? super T> listener; | |
private T currentValue; | |
private SingleChange(ObservableValue<T> observable, ChangeListener<? super T> listener) { | |
super(observable); | |
this.listener = listener; | |
this.currentValue = observable.getValue(); | |
} | |
@Override | |
protected ExpressionHelper<T> addListener(InvalidationListener listener) { | |
return new Generic<T>(observable, listener, this.listener); | |
} | |
@Override | |
protected ExpressionHelper<T> removeListener(InvalidationListener listener) { | |
return this; | |
} | |
@Override | |
protected ExpressionHelper<T> addListener(ChangeListener<? super T> listener) { | |
return new Generic<T>(observable, this.listener, listener); | |
} | |
@Override | |
protected ExpressionHelper<T> removeListener(ChangeListener<? super T> listener) { | |
return (listener.equals(this.listener))? null : this; | |
} | |
@Override | |
protected void fireValueChangedEvent() { | |
final T oldValue = currentValue; | |
currentValue = observable.getValue(); | |
final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue); | |
if (changed) { | |
try { | |
// TODO add null check here? | |
listener.changed(observable, oldValue, currentValue); | |
} catch (Exception e) { | |
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); | |
} | |
} | |
} | |
} | |
private static class Generic<T> extends ExpressionHelper<T> { | |
private OrderedCollection<InvalidationListener> invalidationListeners; | |
private OrderedCollection<ChangeListener<? super T>> changeListeners; | |
/* | |
* Locking mechanism: | |
* | |
* Lists are locked while #fireValueChangedEvent runs. The *first* change to a list during this | |
* process must first make a copy of the list. Once it is copied however, the lock is | |
* irrelevant (unless locked again, potentially by a nested #fireValueChangedEvent?). When a | |
* list is copied, the relevant locking AtomicBoolean is therefore replaced with a new AtomicBoolean | |
* which is unlocked. This prevents making multiple copies when in fact the list was already | |
* copied and wasn't specifically locked again. | |
*/ | |
private AtomicBoolean changeListenersLocked = new AtomicBoolean(); | |
private AtomicBoolean invalidationListenerslocked = new AtomicBoolean(); | |
private T currentValue; | |
private Generic(ObservableValue<T> observable, InvalidationListener listener0, InvalidationListener listener1) { | |
super(observable); | |
this.invalidationListeners = new OrderedCollection<>(new InvalidationListener[] {listener0, listener1}); | |
} | |
private Generic(ObservableValue<T> observable, ChangeListener<? super T> listener0, ChangeListener<? super T> listener1) { | |
super(observable); | |
this.changeListeners = new OrderedCollection<>(List.of(listener0, listener1)); | |
this.currentValue = observable.getValue(); | |
} | |
private Generic(ObservableValue<T> observable, InvalidationListener invalidationListener, ChangeListener<? super T> changeListener) { | |
super(observable); | |
this.invalidationListeners = new OrderedCollection<>(new InvalidationListener[] {invalidationListener}); | |
this.changeListeners = new OrderedCollection<>(List.of(changeListener)); | |
this.currentValue = observable.getValue(); | |
} | |
@Override | |
protected Generic<T> addListener(InvalidationListener listener) { | |
if (invalidationListeners == null) { | |
invalidationListeners = new OrderedCollection<>(List.of(listener)); | |
} else { | |
// final int oldCapacity = invalidationListeners.length; | |
if (invalidationListenerslocked.get()) { | |
invalidationListenerslocked = new AtomicBoolean(); | |
invalidationListeners = new OrderedCollection<>(invalidationListeners); | |
// TODO Contains logic to remove GC'd listeners during the copy -- needs to be resolved differently still | |
// } else if (invalidationSize == oldCapacity) { | |
// invalidationSize = trim(invalidationSize, invalidationListeners); | |
// if (invalidationSize == oldCapacity) { | |
// final int newCapacity = (oldCapacity * 3)/2 + 1; | |
// invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity); | |
// } | |
} | |
invalidationListeners.add(listener); | |
} | |
return this; | |
} | |
@Override | |
protected ExpressionHelper<T> removeListener(InvalidationListener listener) { | |
if (invalidationListeners != null) { | |
if (invalidationListeners.contains(listener)) { | |
if (invalidationListeners.size() == 1) { | |
if (changeListeners != null && changeListeners.size() == 1) { | |
return new SingleChange<>(observable, changeListeners.iterator().next()); | |
} | |
invalidationListeners = null; | |
} | |
else if (invalidationListeners.size() == 2 && changeListeners == null) { | |
Iterator<InvalidationListener> iterator = invalidationListeners.iterator(); | |
InvalidationListener leftOverItem = iterator.next(); | |
return new SingleInvalidation<>(observable, leftOverItem.equals(listener) ? iterator.next() : leftOverItem); | |
} | |
else { | |
if (invalidationListenerslocked.get()) { | |
invalidationListenerslocked = new AtomicBoolean(); | |
invalidationListeners = new OrderedCollection<>(invalidationListeners); | |
} | |
invalidationListeners.remove(listener); | |
} | |
} | |
} | |
return this; | |
} | |
@Override | |
protected ExpressionHelper<T> addListener(ChangeListener<? super T> listener) { | |
if (changeListeners == null) { | |
changeListeners = new OrderedCollection<>(List.of(listener)); | |
} else { | |
// final int oldCapacity = changeListeners.length; | |
if (changeListenersLocked.get()) { | |
changeListenersLocked = new AtomicBoolean(); | |
changeListeners = new OrderedCollection<>(changeListeners); | |
// TODO Contains logic to remove GC'd listeners during the copy -- needs to be resolved differently still | |
// } else if (changeSize == oldCapacity) { | |
// changeSize = trim(changeSize, changeListeners); | |
// if (changeSize == oldCapacity) { | |
// final int newCapacity = (oldCapacity * 3)/2 + 1; | |
// changeListeners = Arrays.copyOf(changeListeners, newCapacity); | |
// } | |
} | |
changeListeners.add(listener); | |
} | |
if (changeListeners.size() == 1) { | |
currentValue = observable.getValue(); | |
} | |
return this; | |
} | |
@Override | |
protected ExpressionHelper<T> removeListener(ChangeListener<? super T> listener) { | |
if (changeListeners != null) { | |
if (changeListeners.contains(listener)) { | |
if (changeListeners.size() == 1) { | |
if (invalidationListeners != null && invalidationListeners.size() == 1) { | |
return new SingleInvalidation<>(observable, invalidationListeners.iterator().next()); | |
} | |
changeListeners = null; | |
} | |
else if (changeListeners.size() == 2 && invalidationListeners == null) { | |
Iterator<ChangeListener<? super T>> iterator = changeListeners.iterator(); | |
ChangeListener<? super T> leftOverItem = iterator.next(); | |
return new SingleChange<>(observable, leftOverItem.equals(listener) ? iterator.next() : leftOverItem); | |
} | |
else { | |
if (changeListenersLocked.get()) { | |
changeListenersLocked = new AtomicBoolean(); | |
changeListeners = new OrderedCollection<>(changeListeners); | |
} | |
changeListeners.remove(listener); | |
} | |
} | |
} | |
return this; | |
} | |
@Override | |
protected void fireValueChangedEvent() { | |
final OrderedCollection<InvalidationListener> curInvalidationList = invalidationListeners; | |
final OrderedCollection<ChangeListener<? super T>> curChangeList = changeListeners; | |
final AtomicBoolean lock1 = this.invalidationListenerslocked; | |
final AtomicBoolean lock2 = this.changeListenersLocked; | |
try { | |
lock1.set(true); | |
lock2.set(true); | |
for (InvalidationListener listener : curInvalidationList) { | |
try { | |
listener.invalidated(observable); | |
} catch (Exception e) { | |
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); | |
} | |
} | |
if (curChangeList != null && !curChangeList.isEmpty()) { | |
final T oldValue = currentValue; | |
currentValue = observable.getValue(); | |
final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue); | |
if (changed) { | |
for (ChangeListener<? super T> listener : curChangeList) { | |
try { | |
listener.changed(observable, oldValue, currentValue); | |
} catch (Exception e) { | |
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); | |
} | |
} | |
} | |
} | |
} finally { | |
lock1.set(false); | |
lock2.set(false); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment