Skip to content

Instantly share code, notes, and snippets.

@hjohn
Created July 8, 2022 20:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hjohn/f88362ea78adef96f3a54d97e2405076 to your computer and use it in GitHub Desktop.
Save hjohn/f88362ea78adef96f3a54d97e2405076 to your computer and use it in GitHub Desktop.
ExpressionHelper using a different collection for large lists and different locking
/*
* 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